/*
 * Copyright (c) 2012 Johan Fylling
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy 
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights 
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
 * copies of the Software, and to permit persons to whom the Software is 
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in 
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
 * THE SOFTWARE.
 */

part of engine;


abstract class CollidablePolygon extends Collidable {
  final List<Vector2> _normals;
  final List<Vector2> _vertices;
  
  CollidablePolygon () : this._normals = new List<Vector2>(),
                         this._vertices = new List<Vector2>();
  
  /** Add normal. */
  void addNormal (Vector2 normal) => this._normals.add(normal);
  
  /** Remove all normals. */
  void clearNormals () => this._normals.clear();
  
  /** Vertices must be added in a counter-clockwise order. */
  void addVertex (Vector2 vertex) => this._vertices.add(vertex);
  
  /** Remove all vertices. */
  void clearVertices () => this._vertices.clear();
  
  /** See [Collidable.getFoci]. */
  void getFoci (List<Vector2> foci, DynamicBody body) {
    // Polygons doesn't have foci.
  }
  
  /** See [Collidable.getAxes]. */
  void getAxes (List<Vector2> axes, List<Vector2> foci, DynamicBody body) {
    if (this._normals.isEmpty) {
      return;
    }
    
    // Add each normal of the polygon as an axis.
    for (Vector2 normal in this._normals) {
      axes.addLast(new Vector2.fromAngle(body.angle, normal));
    }
    
    // Generate axes from foci.
    for (Vector2 focus in foci) {
      Vector2 closestVertex = new Vector2();
      num closestDistance = 0.0;
      
      bool firstPass = true;
      for (Vector2 vertex in this._vertices) {
        Vector2 translatedVertex = body.position + (body.angle == 0.0 ? vertex : new Vector2.fromAngle(body.angle, vertex)*vertex.length);
        num distance = (focus - translatedVertex).lengthSquared;
        if (firstPass || distance < closestDistance) {
          firstPass = false;
          closestDistance = distance;
          closestVertex = translatedVertex;
        }
      }
      
      axes.addLast((focus - closestVertex).normalized());
    }
  }
  
  /** See [Collidable.project]. */
  Interval project (Vector2 axis, DynamicBody body) {
    num min = 0.0;
    num max = 0.0;
    bool firstPass = true;
    
    for (Vector2 vertex in this._vertices) {
      Vector2 transformedVertex = new Vector2.fromAngle(body.angle, vertex);
      transformedVertex = transformedVertex.normalized() * vertex.length + body.position;
      num dotProduct = axis.dot(transformedVertex);
      
      if (firstPass) {
        min = dotProduct;
        max = dotProduct;
        firstPass = false;
      } else if (dotProduct < min) {
        min = dotProduct;
      } else if (dotProduct > max) {
        max = dotProduct;
      }
    }
    
    return new Interval(min, max);
  }
  
  /** See [Collidable.getFarthestFeature]. */
  ContactFeature getFarthestFeature (DynamicBody body, Vector2 direction) {
    if (this._vertices.isEmpty) {
      return new ContactVertex(new Vector2.copy(body.position));
    }
    
    if (this._vertices.length == 1) {
      Vector2 vertex = new Vector2.fromAngle(body.angle, this._vertices[0]);
      vertex = body.position + vertex.normalized() * this._vertices[0].length;
      return new ContactVertex(vertex);
    }
    
    if (this._vertices.length == 2) {
      Vector2 vertex1 = new Vector2.fromAngle(body.angle, this._vertices[0]);
      vertex1 = body.position + vertex1.normalized() * this._vertices[0].length;
      
      Vector2 vertex2 = new Vector2.fromAngle(body.angle, this._vertices[1]);
      vertex2 = body.position + vertex2.normalized() * this._vertices[1].length;
      
      return new ContactEdge(vertex1, vertex2, (direction.dot(vertex1) > direction.dot(vertex2) ? vertex1 : vertex2));
    }
    
    int middleIndex = 0;
    num max = 0;
    Vector2 middleVertex = null;
    for (int i = 0; i < this._vertices.length; i++) {
      Vector2 transformedVertex = new Vector2.fromAngle(body.angle, this._vertices[i]);
      transformedVertex = body.position + transformedVertex.normalized() * this._vertices[i].length;
      num projection = direction.dot(transformedVertex);
      
      if (middleVertex == null || projection > max) {
        middleIndex = i;
        max = projection;
        middleVertex = transformedVertex;
      }
    }
    
    int leftIndex = (middleIndex + 1 >= this._vertices.length ? 0 : middleIndex + 1);
    int rightIndex = (middleIndex == 0 ? this._vertices.length -1 : middleIndex - 1);
    
    Vector2 leftVertex = new Vector2.fromAngle(body.angle, this._vertices[leftIndex]);
    leftVertex = body.position + leftVertex.normalized() * this._vertices[leftIndex].length;
    Vector2 leftDirection = (leftVertex - middleVertex).normalized();
    
    Vector2 rightVertex = new Vector2.fromAngle(body.angle, this._vertices[rightIndex]);
    rightVertex = body.position + rightVertex.normalized() * this._vertices[rightIndex].length;
    Vector2 rightDirection = (middleVertex - rightVertex).normalized();
    
    if (direction.dot(leftDirection).abs() < direction.dot(rightDirection).abs()) {
      return new ContactEdge(leftVertex, middleVertex, middleVertex);
    } else {
      return new ContactEdge(middleVertex, rightVertex, middleVertex);
    }
  }
  
  /** See [Collidable.raycast]. */
  bool raycast (Vector2 from, Vector2 to, DynamicBody body) {
    if (this._vertices.length <= 1) {
      return false;
    }
    
    bool intersection = false;
    num distance = 0.0;
    Vector2 lastVertex = this._vertices.last;
    Vector2 rayEndOffset = to - from;
    
    for (Vector2 vertex in this._vertices) {
      Vector2 lineStart = body.position + (body.angle == 0.0 ? lastVertex : new Vector2.fromAngle(body.angle, lastVertex)*lastVertex.length);
      Vector2 lineEnd = body.position + (body.angle == 0.0 ? vertex : new Vector2.fromAngle(body.angle, vertex)*vertex.length);
      Vector2 lineEndOffset = lineEnd - lineStart;
      
      num dividend = (lineStart - from).perpDot(lineEndOffset);
      num divisor = rayEndOffset.perpDot(lineEndOffset);
      
      if (dividend != 0.0 && divisor != 0.0) {
        num t = dividend/divisor;
        if (0.0 <= t && t <= 1.0) {
          num innerDividend = (lineStart - from).perpDot(rayEndOffset);
          if (innerDividend != 0.0) {
            num u = innerDividend/divisor;
            if (0.0 <= u && u <= 1.0) {
              Vector2 point = from + rayEndOffset*t;
              num currentDistance = (point - from).lengthSquared;
              if (!intersection || currentDistance <= distance) {
                distance = currentDistance;
                to.clone(point);
              }
              intersection = true;
            }
          }
        }
      }
      lastVertex = vertex;
    }
    return intersection;
  }
  
  /** See [Collidable.getRandomPoint]. */
  Vector2 getRandomPoint (DynamicBody body, Math.Random random) {
    // TODO: Calculate random point inside polygon.
    return body.position;
  }
  
  toJson () {
    var data = super.toJson();
    assert(data is Map);
    data["type"] = "collidable_polygon";
    data["normals"] = _normals;
    data["vertices"] = _vertices;
    return data;
  }
}
